#ifndef __VANILA_VIRTUAL_MACHINE_HH__
#define __VANILA_VIRTUAL_MACHINE_HH__

#include "vanila/baseobject.h"
#include "vanila/dsobject.h"
#include "vanila/object.h"
#include "vanila/opcode.h"
#include "vanila/chunk.h"
#include "vanila/value.h"
#include "vanila/callframe.h"
#include "utils/singleton.h"
#include <string>
#include <vector>
#include <list>
#include <map>

namespace vanila
{
class VirtualMachine : public utils::Singleton<VirtualMachine>
{
    friend class Native;
    
    enum { STACK_MAX = 256, FRAMES_MAX = 256 };
    using Self = VirtualMachine;

public:
    //! \brief Virtual Machine constructor and destructor
    VirtualMachine() noexcept;
    ~VirtualMachine() noexcept {}

public:
    //! \brief initial the virtual machine's resource
    void init() noexcept;

    //! \brief release the virtual machine's resource
    void release() noexcept;

    //! \brief register native functions
    void registerNativeFunctions();

    //! \brief register native classes
    void registerNativeClasses();

public:
    //! \brief interpret the source code
    void interpret(const std::string& source);

    //! \brief a runtime error happend, throw a RuntimeError 
    void runtimeError(const std::string& message);

public:
    //! \brief insert a object to object list
    void insertObject(Object* object)
    { this->_objects.push_back(object); }

    //! \brief find specify hashvalue in string tables
    ObjectString* findString(uint32_t hash) const;
    
    //! \brief find specfty string
    ObjectString* findString(const char* chars, uint32_t length) const;

    //! \brief add a new string object with hash value
    void insertString(uint32_t hash, ObjectString* string)
    { this->_strings.insert({hash, string}); }

    //! \brief execute the instance's method
    Value callIntanceMethod(const ObjectInstance* instance, const Value& method, const std::vector<Value>& arguments = {});

private:
    //! \brief push the value into stacktop
    void push(Value value) noexcept
    { *((this->_stackTop)++) = value; }

    //! \brief pop the value at stack top
    Value& pop() noexcept
    { return *(--(this->_stackTop)); }

    //! \brief peek the value which from distance to top
    Value& peek(int distance) noexcept
    { return this->_stackTop[-1 - distance]; }

    //! \brief print stack
    void printStack();

    //! \brief reset the vm's stack
    void resetStack() noexcept;

private:
    //! \brief read the bytecode in ip
    OpCode readBytecode() noexcept
    { return *(this->_currentFrame->ip++); }

    //! \brief read the byte value in ip
    uint8_t readByte() noexcept
    { return static_cast<uint8_t>(this->readBytecode()); }

    //! \brief read the short value in ip
    uint16_t readShort() noexcept;

    //! \brief read the current constant value
    Value readConstant()
    { return this->_currentFrame->closure->function()->chunk().constants()[ this->readByte() ]; }

    // \brief read the string which ip point as
    ObjectString* readString()
    { return this->readConstant().asString(); }

private:
    //! \brief operator bewteen two operand
    //! \note operate the two value in stack top
    Value binaryOperate(OpCode operatorType);

    //! \brief check wheather the value is a 'false' value
    bool isFalsey(const Value& value) const noexcept
    { return value.isNil() || (value.isBoolean() && !value.boolean() ); }

private:
    //! \brief use the suitable way to call a calle value incroding to it's object type
    bool callValue(Value callee, int argCount);

    //! \brief create a new frames to call a closure object with specify arguments
    bool call(ObjectClosure* function, int argCount);

    //! \brief invoke call(Closure or Native)
    bool invokeCall(Object* callee, int argCount);

    //! \brief try invoke a method
    void tryInvoke(ObjectString* name, int argCount);

    //! \brief try to find the mrthod 'name' in klass's _methods, and call it
    void invokeFromClass(ObjectClass* klass, ObjectString* name, int argCount);

private:
    //! \brief try to find a UpvalueObject which manage the specify Value* in _openUpvalues list
    //! \brief if find return it, or create a new UpvalueObject and link it
    ObjectUpvalue* captureUpvalue(Value* local);

    //! \brief define a native function
    void defineNative(const char* name, NativeFunction function);

    //! \brief define a method
    void defineMethod(ObjectString* name);

    //! \brief try to find the method 'name' in klass's method table
    //! \brief if so, it push the method into the stack and return true
    void tryBindMethod(ObjectClass* klass, ObjectString* name);

    //! \brief 'close' the upvalue variable 
    void closeUpvalues(Value* last) noexcept;

private:
    //! \brief virtual machine run!!
    void run(size_t finishFrameSize = 0);

private:
    std::vector<CallFrame> _frames;
    CallFrame* _currentFrame;

    Value _stack[STACK_MAX];
    Value* _stackTop;

    std::list<Object*> _objects;
    std::map<uint32_t, ObjectString*> _strings;
    ObjectUpvalue* _openUpvalues;
    
    std::map<ObjectString*, Value> _globals;
    
    bool _showStack;
    bool _showBytecodes;

public:
    Value* stack() noexcept
    { return this->_stack; }

    Value* stackTop() noexcept
    { return this->_stackTop; }

    void setShowStack(bool enable) noexcept
    { this->_showStack = enable; }

    void setShowBytecodes(bool enable) noexcept
    { this->_showBytecodes = enable; }

    bool showBytecodes() const noexcept
    { return this->_showBytecodes; }

    std::vector<CallFrame>& frames() noexcept
    { return this->_frames; }

    ObjectUpvalue* openUpvalues() noexcept
    { return this->_openUpvalues; }

    std::map<ObjectString*, Value>& globals() noexcept
    { return this->_globals; }

    std::list<Object*>& objects() noexcept
    { return this->_objects; }

    std::map<uint32_t, ObjectString*>& strings() noexcept
    { return this->_strings; }
};

}

#endif